﻿//-----------------------------------------------------------------------
// <copyright file="MessageManager.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace TileTool.Base.Message
{
    using System;
    using System.Collections.Generic;
    using System.Threading;
    using TileTool.Message;

    /// <summary>
    /// Base implementation of the message manager interface.
    /// </summary>
    public class MessageManager : IMessageManager
    {
        #region Constants - Private

        /// <summary>
        /// Number of message queues we have.
        /// </summary>
        private const int MessageQueueCount = 2;

        #endregion

        #region Fields - Private

        /// <summary>
        /// Value that we set to denote that we are dispatching. If 0 we are not dispatching and if 1
        /// we are in the process of dispatching.
        /// </summary>
        private long dispatching = 0;

        /// <summary>
        /// Counter which counts up for each registration and assigns an ID to that registration.
        /// </summary>
        private ulong registrationIdCounter = 1;

        /// <summary>
        /// Current message queue to add items to.
        /// </summary>
        private int currentQueue = 0;

        /// <summary>
        /// Array of message queues we have.
        /// </summary>
        private IList<PendingMessage>[] messageQueue = new IList<PendingMessage>[MessageQueueCount];

        /// <summary>
        /// Dictionary that contains all of our message handlers.
        /// </summary>
        private IDictionary<Type, HandlerInstanceList> handlerDictionary = new Dictionary<Type, HandlerInstanceList>();

        /// <summary>
        /// List of handler instances that we will call. Built during a call to DispatchMessages() for each
        /// message that is processed.
        /// </summary>
        private List<MessageHandlerInstance> callList = new List<MessageHandlerInstance>(128);

        #endregion

        #region Constructor

        /// <summary>
        /// Construct the message manager.
        /// </summary>
        public MessageManager()
        {
            this.messageQueue[0] = new List<PendingMessage>();
            this.messageQueue[1] = new List<PendingMessage>();
        }

        #endregion

        #region Methods - Public

        /// <summary>
        /// Called to deliver all messages. Messages should be delivered immediately when this is called.
        /// </summary>
        public void DispatchMessages()
        {
            // If we are told to dispatch messages while we are in the process of
            // dispatching, ignore the request.
            if (Interlocked.CompareExchange(ref this.dispatching, 1, 0) == 1)
            {
                return;
            }

            try
            {
                IList<PendingMessage> processQueue = null;

                processQueue = this.messageQueue[this.currentQueue];

                lock (this)
                {
                    this.currentQueue = (this.currentQueue + 1) & 1;
                }

                if (processQueue.Count != 0)
                {
                    this.ProcessMessageQueue(processQueue);
                    processQueue.Clear();
                }
            }
            finally
            {
                this.dispatching = 0;
            }
        }

        /// <summary>
        /// Send a message on the passed channel.
        /// 
        /// If the channel is not the global channel, the message must be delivered to consumers listening to
        /// the channel as well as those listening to the global channel.
        /// </summary>
        /// <param name="channel">Channel to send the message on. If null, the global channel will be used.</param>
        /// <param name="msg">Message to send.</param>
        public void SendMessage(MessageChannel channel, MessageBase msg)
        {
            if (msg == null)
            {
                throw new ArgumentNullException("msg", "Message must be non-null.");
            }

            if (channel == null)
            {
                channel = MessageChannel.Global;
            }

            MessageBase clone = msg.Clone();

            clone.Validate();

            PendingMessage item = new PendingMessage(channel, clone);

            lock (this)
            {
                IList<PendingMessage> targetQueue = this.messageQueue[this.currentQueue];
                targetQueue.Add(item);
            }
        }

        /// <summary>
        /// Register a handler for the passed type of message.
        /// </summary>
        /// <typeparam name="T">Type of message to handle</typeparam>
        /// <param name="channel">Channel to listen to the message for on. If null, the global channel will be used.</param>
        /// <param name="handler">Message handler to call when the message is received.</param>
        /// <param name="priority">Message handler priority</param>
        /// <param name="filter">Filter to use for message filtering.</param>
        public void RegisterForMessage<T>(
            MessageChannel channel,
            MessageHandler<T> handler,
            int priority = MessageHandlerPriority.Default,
            IMessageFilter filter = null) where T : MessageBase
        {
            if (channel == null)
            {
                channel = MessageChannel.Global;
            }

            if (handler == null)
            {
                throw new ArgumentNullException("handler", "Handler must be non-null.");
            }

            priority = MessageHandlerPriority.Clamp(priority);

            lock (this.handlerDictionary)
            {
                Type type = typeof(T);
                HandlerInstanceList list = null;
                if (!this.handlerDictionary.TryGetValue(type, out list))
                {
                    list = new HandlerInstanceList();
                    this.handlerDictionary.Add(type, list);
                }

                MessageHandlerInstance inst = new MessageHandlerInstance(
                    this.registrationIdCounter,
                    priority,
                    channel,
                    filter,
                    new GenericHandlerCaller<T>(handler));

                this.registrationIdCounter++;

                list.Add(inst);
            }
        }

        /// <summary>
        /// Unregister a handler for the passed type of message.
        /// </summary>
        /// <typeparam name="T">Type of message to handle</typeparam>
        /// <param name="channel">Channel the handler was registered on. If null, the global channel will be used.</param>
        /// <param name="handler">Message handler to remove.</param>
        public void UnregisterForMessage<T>(MessageChannel channel, MessageHandler<T> handler) where T : MessageBase
        {
            if (channel == null)
            {
                channel = MessageChannel.Global;
            }

            if (handler == null)
            {
                throw new ArgumentNullException("handler", "Handler must be non-null.");
            }

            lock (this.handlerDictionary)
            {
                Type type = typeof(T);
                HandlerInstanceList list = null;

                // If we don't have any entries for the passed type, we can return.
                if (!this.handlerDictionary.TryGetValue(type, out list))
                {
                    return;
                }

                // Always use an ID of 0 and priority of default since they are not used for equality tests.
                MessageHandlerInstance inst = new MessageHandlerInstance(
                    0,
                    MessageHandlerPriority.Default,
                    channel,
                    null,
                    new GenericHandlerCaller<T>(handler));

                // Remove the item from the list if we have it.
                //
                // The MessageHandlerInstance class overloads equals to compare channel and handler
                // correctly.
                list.Remove(inst);
            }
        }

        #endregion

        #region Methods - Private

        /// <summary>
        /// Deliver all messages in the list to consumers. It is assumed that the passed queue
        /// is locked and is safe to access.
        /// </summary>
        /// <param name="queue">Queue to process.</param>
        private void ProcessMessageQueue(IList<PendingMessage> queue)
        {
            foreach (PendingMessage pending in queue)
            {
                MessageBase msg = pending.Message;
                MessageChannel channel = pending.Channel;
                Type msgType = msg.GetType();

                // Always clear the call list before doing any processing.
                this.CallListClear();

                // Push all interfaces the message type implements first.
                Type[] interfaces = msgType.GetInterfaces();
                foreach (Type msgInter in interfaces)
                {
                    this.CallListPopulate(msgInter, channel);
                }

                while (msgType != null)
                {
                    // Build the call list based on the current type.
                    this.CallListPopulate(msgType, channel);

                    msgType = msgType.BaseType;
                }

                // Sort the call list based on registration ID so that handlers registered
                // earlier see messages before those registered later
                this.CallListSort();

                this.CallListExecute(msg);
            }
        }

        /// <summary>
        /// Remove all items from the call list.
        /// </summary>
        private void CallListClear()
        {
            this.callList.Clear();
        }

        /// <summary>
        /// Given a message type and the channel it is being sent out, build up our
        /// call list for any message handlers that are valid.
        /// </summary>
        /// <param name="msgType">Message type we are handling.</param>
        /// <param name="channel">Message channel the message was sent on.</param>
        private void CallListPopulate(Type msgType, MessageChannel channel)
        {
            lock (this.handlerDictionary)
            {
                HandlerInstanceList handlers = null;

                // If we don't have any entries for the passed type, we can return.
                if (!this.handlerDictionary.TryGetValue(msgType, out handlers))
                {
                    return;
                }

                foreach (MessageHandlerInstance handlerInst in handlers)
                {
                    // If we don't have a channel match, we check to see
                    // if the handler is listening on the global channel.
                    if (handlerInst.Channel != channel)
                    {
                        // If the handler isn't listening on the global channel,
                        // we can ignore them.
                        if (handlerInst.Channel != MessageChannel.Global)
                        {
                            continue;
                        }
                    }

                    this.callList.Add(handlerInst);
                }
            }
        }

        /// <summary>
        /// Sort all items in the call list based on their registration ID and priority.
        /// 
        /// Priority is used to sort first, followed by registration ID.
        /// </summary>
        private void CallListSort()
        {
            this.callList.Sort((MessageHandlerInstance a, MessageHandlerInstance b) =>
            {
                // If a has greater priority than b, we want it to sort
                // before it.
                if (a.Priority > b.Priority)
                {
                    return -1;
                }

                if (a.Priority < b.Priority)
                {
                    return 1;
                }

                if (a.ID == b.ID)
                {
                    return 0;
                }
                else if (a.ID > b.ID)
                {
                    return 1;
                }
                else
                {
                    return -1;
                }
            });
        }

        /// <summary>
        /// Execute the call list by calling handlers in it until the message is handled.
        /// </summary>
        /// <param name="msg">Message to distribute.</param>
        private void CallListExecute(MessageBase msg)
        {
            foreach (MessageHandlerInstance inst in this.callList)
            {
                if (inst.Filter != null)
                {
                    if (!inst.Filter.Filter(msg))
                    {
                        continue;
                    }
                }

                MessageHandlerResult res = inst.Caller.Call(msg);

                if (res == MessageHandlerResult.Handled)
                {
                    break;
                }
            }
        }

        #endregion

        #region Classes - Private

        /// <summary>
        /// Pending message item that contains a message and the channel it was
        /// sent on.
        /// </summary>
        private class PendingMessage
        {
            /// <summary>
            /// Construct the pending message item.
            /// </summary>
            /// <param name="channel">Channel to send the message on.</param>
            /// <param name="msg">Message to send.</param>
            public PendingMessage(MessageChannel channel, MessageBase msg)
            {
                this.Channel = channel;
                this.Message = msg;
            }

            /// <summary>
            /// Gets the channel the message is being sent on.
            /// </summary>
            public MessageChannel Channel
            {
                get;
                private set;
            }

            /// <summary>
            /// Gets the message.
            /// </summary>
            public MessageBase Message
            {
                get;
                private set;
            }
        }

        /// <summary>
        /// Pretty name around a list of MessageHandlerInstance.
        /// </summary>
        private class HandlerInstanceList : List<MessageHandlerInstance>
        {
        }

        #endregion
    }
}
